﻿package de.gameduell.framework.game.billard.controller {
	import de.gameduell.framework.debug.Debug;
	import de.gameduell.framework.game.billard.event.PhysicEvent;
	import de.gameduell.framework.game.billard.model.Ball;
	import de.gameduell.framework.game.billard.model.BillardSettings;
	import de.gameduell.framework.game.billard.model.Cushion;
	import de.gameduell.framework.game.billard.model.Hole;
	import de.gameduell.framework.game.billard.model.ShootData;
	import de.gameduell.framework.queue.GDAction;
	
	import org.cove.ape.APEngine;
	import org.cove.ape.Group;
	import org.cove.ape.Vector;
	
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.TimerEvent;
	import flash.utils.Timer;	

	/**
	 * holds the different physicengine instances and manages them
	 */
	public class PhysicController extends EventDispatcher {

		private static const HOLE:String = "hole_";
		private static const BALL:String = "ball_";
		private static const CUSHION:String = "cushion_";

		private var _balls:Array;
		private var _cushions:Array;
		private var _holes:Array;

		private var _apEngine:BillardEngine;
		private var _aimingEngine:BillardEngine;	

		private var _ghostBall:Ball;
		private var _whiteBall:Ball;
		private var _calculating:Boolean;
		private var stepTimer:Timer;
		private var content:Sprite;
		private var shootData:ShootData;
		private var settings:BillardSettings; 
		private var _pocketedBalls:Array;

		public function PhysicController(aContent:Sprite,aShootData:ShootData,aSettings:BillardSettings) {
			Debug.trace("PhysicController::PhysicController");
			_balls = new Array();
			_cushions = new Array();
			_holes = new Array();
			_pocketedBalls = new Array();
			stepTimer = new Timer(1, 1);
			stepTimer.addEventListener(TimerEvent.TIMER, calcBallPositions);
			//			isBreakShot = true;
			shootData = aShootData;
			settings = aSettings;
//			init(aContent);	
			content = aContent;
		}

		public function init(ballNum:int,cushionNum:int,holeNum:int):void {
			_apEngine = new BillardEngine();
			
			_apEngine.init(0.3);
			_apEngine.damping = 1;	
			_apEngine.container = new Sprite();
			//			content.addChild(_apEngine.container);

			_apEngine.addGroup(new Group(_apEngine, true));
			_apEngine.cushionLessRectangle = (content.getChildByName("cushionLessSpace") as Sprite).getBounds(content);
			
			create(BALL, ballNum);//16
			create(CUSHION, cushionNum);//18
			createAdditonalCushions();
										
			_aimingEngine = _apEngine.cloneBillardEngine();
			_aimingEngine.container = new Sprite();
			
			_whiteBall = _apEngine.whiteBall;
			_ghostBall = _aimingEngine.whiteBall;
			
			create(HOLE, holeNum);//6
			
			traceBalls();
		}

		/**
		 * traces the ball positions in an xml format
		 */
		public function traceBalls():void {
			Debug.trace("------------ xmlPos ------------");
			var xmlPos:String = "";
			for(var i:* in balls) {
				var ball:Ball = balls[i];
//				xmlPos = xmlPos + "&lt;ball x='" + ball.position.x + "' y='" + ball.position.y + "'/&gt;";
				xmlPos = xmlPos + ball.position.x + "," + ball.position.y+";";
			}
			Debug.trace(xmlPos);
		}

		/**
		 * some cushions in the corner of the table
		 */
		private function createAdditonalCushions():void {
			var cushionIds:Array = new Array(18, 20, 22, 24, 26, 28);
			for(var i:* in cushionIds) {			
				var clip:Sprite = content.getChildByName(CUSHION + cushionIds[i]) as Sprite;
				_cushions.push(Cushion.createParticle(_apEngine, clip));	
			}
		}

		/**
		 * generic creation funtion to create holes balls and cushions
		 */
		public function create(type:String,num:uint):void {	
			for(var i:int = 0;i < num;i++) {
				var clip:Sprite = content.getChildByName(type + i) as Sprite;			
				switch(type) {
					case HOLE:
						var hole:Hole = new Hole(_apEngine, clip, i);
						var holeEngine:HoleEngine = new HoleEngine(content, hole, i);
						_holes.push(holeEngine);
						break;
					case BALL:
						_balls.push(new Ball(settings,_apEngine, clip, i));
						break;
					case CUSHION:
						_cushions.push(Cushion.createParticle(_apEngine, clip));
						break;
				}
			}
		}

		/**
		 * the billardEngine wich manage the ghostball
		 */
		public function get aimingEngine():BillardEngine {
			return _aimingEngine;
		}

		/**
		 * the billardEngine wich manage the balls int the game
		 */
		public function get apEngine():BillardEngine {
			return _apEngine;
		}

		/**
		 * the balls of the game (physical representation)
		 */
		public function get balls():Array {
			return _balls;
		}

		/**
		 * the whiteBall of the aimingEngine
		 */
		public function get ghostBall():Ball {
			return _ghostBall;
		}

		/**
		 * the whiteBall of apeEngine
		 */
		public function get whiteBall():Ball {
			return _whiteBall;
		}

		/**
		 * place the balls of the apeEngine to the positions in the array
		 */
		public function placeBalls(positionArray:Array):void {
			for(var i:int = 0;i < positionArray.length;i++) {
				var position:Vector = positionArray[i];
				moveBall(i, position);
			}
			dispatchEvent(new PhysicEvent(PhysicEvent.BALL_PLACED));
		}

		/**
		 * move a ball with the given index to the given position
		 */
		public function moveBall(index:int,position:Vector):void {
			var movedball:Ball = _balls[index];
			movedball.position = position;
			movedball.bringBackToTable();
			movedball.update(1, true);
			dispatchEvent(new PhysicEvent(PhysicEvent.BALL_MOVED));
		}

		/**
		 * dispatches the PhysicEvent.BALL_PLACED
		 */
		public function placedBall():void {
			dispatchEvent(new PhysicEvent(PhysicEvent.PLACED_CUE_BALL));
			dispatchEvent(new PhysicEvent(PhysicEvent.BALL_PLACED));
		}

		/**
		 * dispatches the PhysicEvent.BALL_PLACED_BREAK
		 */
		public function placedBreakBall():void {
			dispatchEvent(new PhysicEvent(PhysicEvent.PLACED_BREAK_CUEBALL));
			dispatchEvent(new PhysicEvent(PhysicEvent.BALL_PLACED));
		}

		/**
		 * dispatches the PhysicEvent.QUEUE_SHOT
		 */
		public function queueShot():void {
			dispatchEvent(new PhysicEvent(PhysicEvent.QUEUE_SHOT));
		}

		/**
		 * dispatches the PhysicEvent.BALL_REMOVED
		 */
		public function ballReset():void {
			dispatchEvent(new PhysicEvent(PhysicEvent.BALL_RESET));
		}

		/**
		 * calculates the position of the ghostBall
		 */
		public function calcGhost():int {	
			for each(var ball:Ball in _aimingEngine.balls) {
				ball.deletePath();
			}	
			if (!_calculating && ghostBall != null) {
				
				_aimingEngine.synchronizeBalls(apEngine);	//TODO Check copied properties
						
				_ghostBall.velocity = shootData.velocity;
				_ghostBall.spin = shootData.effet;			
				_calculating = true;			

				// reset ball before hit (PhysicsController.calcGhost & PhysicsController.hitWhiteBall)
				for each (var b:Ball in aimingEngine.balls) {
					b.updateAfterContactOrResting();
				}
	
				// min one step
				_aimingEngine.stepOptimized();
				
				// before collision		
	
				var index:int = 0;
				while(!ghostBall.hasCollisionLastMajorFrame() && !ghostBall.isStopped() && index < 200) { 
//				while(continueGhostBallCalculation() && index < 200) { 
					_aimingEngine.stepOptimized();
					index++;
				}
				
				// after collision
//				var stepsAfterCollision:Number = _ghostBall.velocity.magnitude() / settings.getValue(BillardSettings.KEY_MAX_SPEED);
				for(var i:int = 0;i < settings.getValue(BillardSettings.KEY_GHOSTSTEPS_AFTER_COLLISION);i++) {
					_aimingEngine.stepOptimized();
				}		
				_calculating = false;	
				return index;	
			}
			return 0;
		}

		public function continueGhostBallCalculation():Boolean {		
			if(_ghostBall.isStopped()) {
				return false;
			}
			if(_ghostBall.hasCollisionLastMajorFrame()) {
				// checks if the collision had any effect
				var ballCollision:Boolean = false;
				var collidedBallIds:String = "";
				for(var j in _aimingEngine.balls) {
					var ball:Ball = _aimingEngine.balls[j];
					if(ball.hasCollisionLastMajorFrame() && ball != _ghostBall) {
						ballCollision = true;
						collidedBallIds = collidedBallIds+","+ball.id;
						if(!ball.isStopped()) {
							return false;
						}
					}				
				}
				if(ballCollision == false) {
					return false;
				}
				return true;
			}else {
				return true;
			}
		}

		// called only by Action for queueing
		public function hitWhiteBall():void {	
			dispatchEvent(new PhysicEvent(PhysicEvent.CALCULATION_START));
			
			_whiteBall.velocity = shootData.velocity;
			_whiteBall.spin = shootData.effet;
			_calculating = true;
			
			for each(var b:Ball in balls) {
				b.updateAfterContactOrResting();
				b.clearRuleInfo();
			}
			_apEngine.clearRuleInfo();
			_pocketedBalls.length = 0;
			calcBallPositions();		
		}

		public function whiteBallPositionValid():Boolean {
			for(var i:* in apEngine.balls) {
				var ball:Ball = apEngine.balls[i];
				if(ball != whiteBall) {
					if(whiteBall.position.distance(ball.position) < ball.radius * 2) {
						return false;
					}
				}
			}
			return true;
		}

		// called first by hitWhiteBall() and after by timer for threating reasons
		private function calcBallPositions(event:Event = null):void {
					
			// 20 steps adaptive (integrate, check collisions and backStep)

			calcSteps(20);
			
			// balls are still moving ...
			var ballsStopped:Boolean = apEngine.ballsStopped();
			if(event == null || !ballsStopped) {	
				stepTimer.reset();
				stepTimer.start();
			}		
			// balls are stopped yet ...
			else {		
				var holeEngine:HoleEngine = null;
				for(var i:int = 0;i < _balls.length;i++) {
					var ball:Ball = _balls[i];

					if(ball.hole != null) {	
						var keyValue:String = ball.id + "=" + ball.hole.id;
						_pocketedBalls.push(keyValue);	
						holeEngine = _holes[ball.hole.id];
						ball.addToHole(holeEngine);
						ball.removeFromHole(apEngine);							
						ball.position = new Vector(-1000, -1000);
						ball.createStep();
					}			
						
					ball.clearBackStepCache();	
				}
				Debug.trace("PhysicController::calcBallPositions calc done");
				_calculating = false;

				if(settings.isBreakMode()) {	
					Debug.trace("calcBallPositions switchToBreakMode");	
					new GDAction(settings.switchToNormalMode, [], true, "settings.switchToNormalMode");
//					settings.switchToNormalMode();
//					isBreakShot = false;
				}
				var ballTouched:String = firstBallTouched != null ? firstBallTouched.id + "" : "-1";
				Debug.trace("PhysicController::calcBallPositions firstBallTouched "+ballTouched);
				dispatchEvent(new PhysicEvent(PhysicEvent.CALCULATION_DONE));
			}
		}	
		
		private function calcSteps(stepNum:int):void {
			for(var i:int = 0;i < stepNum;i++) {
				_apEngine.stepOptimized();
			}
		}

		public function enableBreakMode():void {
			settings.switchToBreakMode();
		}

		public function get firstPocketedBall():Ball {
			return _apEngine.firstPocketedBall;
		}

		public function get firstBallTouched():Ball {
			return whiteBall.firstBallTouched;
		}

		public function get touchedCushion():Boolean {
			var touched:Boolean = false;
			for(var i:* in balls) {
				touched = touched || Ball(balls[i]).touchedCushion;
			}
			return touched;
		}
		
		public function get ballsTouchedCushion():int {
			var touchedCounter:int = 0;
			for(var i:* in balls) {
				if(Ball(balls[i]).touchedCushion){
					touchedCounter++;
				}
			}
			return touchedCounter;
		}

		public function get pocketedBalls():Array {
			return _pocketedBalls;
		}

		public function get calculating():Boolean {
			return _calculating;
		}


	}
}
